﻿using System;
using System.IO;
using System.Text;
using System.Web;
using System.Security.Cryptography;
using System.Collections.Generic;
using Microsoft.SharePoint;
using HubKey.Net.FrontPageRPC;
using Ionic.Zip;

namespace SharePointRsync
{
    public class Sync
    {
        private String zipRoot = "C:\\Program Files\\Common Files\\microsoft shared\\Web Server Extensions\\15\\temp\\sharepoint_rsync_temp\\";
        private string _syncFrom, _syncTo = null;
        private bool _createZip = false;
        private bool _isfile = true;
        private bool _forceOverwrite = false;
        private bool _useSyncListMetaData = false;
        private bool _success = true;
        private DateTime _timeToRun = DateTime.Now;
        private MetaInfoCollection _itemMetaInfo = new MetaInfoCollection();
        private List<FileInfo> _existingTargetFiles = new List<FileInfo>();
        private List<DirectoryInfo> _existingTargetDirectories = new List<DirectoryInfo>();
        private SPFieldCollection _sourceFieldCols = null;
        List<string> _noreadCol = new List<string>();
        public List<string> syncLog = new List<string>();

        public Sync(SPListItem item, SPFieldCollection syncListfieldCols, SPFieldCollection sourceFieldCols, bool forceOverwrite)
        {
            _forceOverwrite = forceOverwrite;
            _sourceFieldCols = sourceFieldCols;
            CreateFieldBlacklist();
            SetItemSyncPaths(item);

            switch (CreateZipOrSync(item))
            {
                case "Create":
                    CreateZipFile(item);
                    break;
                case "Complete":
                    _createZip = false;
                    SyncComplete(item);
                    return;
                default:
                    break;
            }


            SetSyncListItemMetaInfo(item, syncListfieldCols);

            try
            {
                if (_isfile)
                    SyncFile();
                else
                {
                    try
                    {
                        DirectoryInfo diTarget = new DirectoryInfo(_syncTo.Remove(0, 5).Replace("/", "\\"));
                        SetExistingTargetFiles(diTarget);
                    }
                    catch
                    {
                        _forceOverwrite = true;
                    }
                    DirectoryInfo diSource = new DirectoryInfo(_syncFrom);
                    SyncFolder(diSource, _syncTo);
                }
                SyncComplete(item);
            }
            catch (Exception ex)
            {
                SyncError(item, ex.Message);
            }
        }

        private string CreateZipOrSync(SPListItem item)
        {
            try
            {                
                if (Path.GetExtension(_syncFrom) == ".zip")
                {
                    //its a zip file so just sync it
                    return "Continue";
                }

                _createZip = item["CreateZip"].ToString().CompareTo("Yes") == 0;

                if (_createZip == false)
                {
                    //no zip so sync directory or file
                    return "Continue";
                }
            }
            catch (Exception ex)
            {
                // the CreateZip column didn't exist or wasn't initialized (i.e. it didn't exist when the sync job was originally created), so just bail
                return "Continue";
            }

            string currentDirectoryHash = GetDirectoryHash();
            string syncToUNC = _syncTo.Remove(0, 5).Replace("/", "\\") + item.Title + ".zip";
            bool fileExists = System.IO.File.Exists(syncToUNC);

            if (GetPreviousDirectoryHash(item) != currentDirectoryHash)
            {
                //directory structure has change so make a zip file
                return "Create";
            }
            else
            {
                if (!fileExists)
                {
                    return "Create";
                }
                else
                {
                    //directory structure has not changed so complete sync
                    syncLog.Add("Directory structure and file properties have not changed so zip file will not be created or synced");
                    return "Complete";
                }
            }
        }

        private void CreateZipFile(SPListItem item)
        {
            String zipDestination = "";
            _isfile = true;
            syncLog.Add("creating zip file of source contents");
            zipDestination = zipRoot + item.Title + ".zip";

            try
            {

                // check if the zip folder exists and create it if not
                if (!System.IO.Directory.Exists(zipRoot))
                {
                    syncLog.Add("creating temporary zip location in '" + zipRoot + "'");
                    System.IO.Directory.CreateDirectory(zipRoot);
                }

                // remove the old file first, if it exists
                if (System.IO.File.Exists(zipDestination))
                {
                    System.IO.File.Delete(zipDestination);
                }

                using (ZipFile zip = new ZipFile())
                {
                    if (System.IO.File.Exists(_syncFrom))
                    {
                        zip.AddFile(_syncFrom, "");
                    }
                    else
                    {
                        zip.AddDirectory(_syncFrom); // recurses subdirectories
                    }

                    // save the zip file
                    zip.Save(zipDestination);

                    // if all is well, replace the sync from with the path to the zip file
                    _syncFrom = zipDestination;
                }
            }
            catch (Exception ex)
            {
                syncLog.Add(ex.Message);
                syncLog.Add("\n zip file creation failed");
            }
        }

        private string GetDirectoryHash()
        {
            string dirListing = string.Empty;

            if (!System.IO.File.Exists(_syncFrom))
            {
                dirListing = RecursiveFolderScan(_syncFrom);
            }
            else
            {
                FileInfo info = new FileInfo(_syncFrom);
                dirListing = info.CreationTimeUtc.ToShortDateString();
                dirListing += info.LastWriteTimeUtc.ToShortDateString();
                dirListing += info.FullName;
            }

            string directoryHash = CalculateMD5Hash(dirListing);
            syncLog.Add("\nDirectoryHash:" + directoryHash.ToString() + "\n");
            return directoryHash.ToString();
        }

        private string GetPreviousDirectoryHash(SPListItem item)
        {
            string log = (string)item["SyncLog"];
            string previousDirectoryHash = string.Empty;

            if (!string.IsNullOrEmpty(log))
            {

                using (StringReader reader = new StringReader(log))
                {
                    string line;
                    while ((line = reader.ReadLine()) != null)
                    {
                        if (line.Contains("DirectoryHash:"))
                        {
                            previousDirectoryHash = line.Split(':')[1];
                        }
                    }
                }
            }

            return previousDirectoryHash;
        }

        private string CalculateMD5Hash(string input)
        {
            // step 1, calculate MD5 hash from input
            MD5 md5 = System.Security.Cryptography.MD5.Create();
            byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input);
            byte[] hash = md5.ComputeHash(inputBytes);

            // step 2, convert byte array to hex string
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < hash.Length; i++)
            {
                sb.Append(hash[i].ToString("X2"));
            }
            return sb.ToString();
        }

        private void CleanUpZipFile(SPListItem item)
        {
            String zipDestination = "";

            if (_createZip)
            {
                syncLog.Add("removing Zip file of source contents");
                zipDestination = zipRoot + item.Title + ".zip";

                try
                {
                    // remove the old file first, if it exists
                    if (System.IO.File.Exists(zipDestination))
                    {
                        System.IO.File.Delete(zipDestination);
                    }
                }
                catch (Exception ex)
                {
                    syncLog.Add(ex.Message);
                }
            }
        }

        private string RecursiveFolderScan(string path)
        {
            string info = string.Empty;

            var dirInfo = new DirectoryInfo(path);
            foreach (FileSystemInfo entry in dirInfo.GetFileSystemInfos())
            {
                bool isDir = (entry.Attributes & FileAttributes.Directory) != 0;
                if (isDir)
                {
                    info += (RecursiveFolderScan(entry.FullName));
                }
                info += entry.CreationTimeUtc.ToShortDateString();
                info += entry.LastWriteTimeUtc.ToShortDateString();
                info += entry.FullName;
            }
            return info;
        }

        private void SetSyncListItemMetaInfo(SPListItem item, SPFieldCollection syncListFieldCols)
        {
            //set meta info based on sync list
            LoopFieldCols(item, syncListFieldCols);
            if (_itemMetaInfo.Count < 2 && _sourceFieldCols != null)
            {
                //no sync lists cols so reset metainfo
                _itemMetaInfo = new MetaInfoCollection();
            }
            else
            {
                _useSyncListMetaData = true;
            }
        }

        private void LoopFieldCols(SPListItem item, SPFieldCollection fieldCols)
        {
            foreach (SPField field in fieldCols)
            {
                if (field.InternalName == "Title")
                    _itemMetaInfo.Add("vti_title", item[field.InternalName]);
                else if (UserCreatedField(field))
                {
                    object value = string.Empty;
                    MetaTypeEnum type = SetMetaEnum(field.Type.ToString());
                    try
                    {
                        value = GetFormattedMetaValue(item, field);
                    }
                    catch { }
                    _itemMetaInfo.Add(field.InternalName, type, value);
                }
            }
        }

        private void SetExistingTargetFiles(DirectoryInfo target)
        {
            // Copy each file into it's new directory.
            foreach (FileInfo targetFile in target.GetFiles())
            {
                if ((targetFile.Attributes & FileAttributes.Hidden) != FileAttributes.Hidden)
                    _existingTargetFiles.Add(targetFile);
            }

            // Copy each subdirectory using recursion.
            foreach (DirectoryInfo sourceSubDir in target.GetDirectories())
            {
                if ((sourceSubDir.Attributes & FileAttributes.Hidden) != FileAttributes.Hidden)
                {
                    _existingTargetDirectories.Add(sourceSubDir);
                    SetExistingTargetFiles(sourceSubDir);
                }
            }
        }

        private void SyncFolder(DirectoryInfo source, string targetDirectory)
        {
            foreach (FileInfo sourceFile in source.GetFiles())
            {
                if (ShouldCopyFile(sourceFile))
                    CopyFile(sourceFile.FullName, Path.Combine(targetDirectory, sourceFile.Name));
                else
                    syncLog.Add("File not copied because source not changed:" + sourceFile.Name);
            }
            foreach (DirectoryInfo sourceSubDir in source.GetDirectories())
            {
                if ((sourceSubDir.Attributes & FileAttributes.Hidden) != FileAttributes.Hidden)
                    SyncFolder(sourceSubDir, targetDirectory + sourceSubDir.Name + "/");
            }
        }

        private void SyncFile()
        {
            string filename = Path.GetFileName(_syncFrom);
            string targetPath = Path.Combine(_syncTo, filename);
            FileInfo sourceFile = new FileInfo(_syncFrom);

            if (sourceFile.Exists == false)
                throw new Exception("File does not exist");
            if (_forceOverwrite)
                CopyFile(_syncFrom, targetPath);
            else
            {
                try
                {
                    FileInfo targetFile = new FileInfo(targetPath.Remove(0, 5).Replace("/", "\\"));
                    if (FilesDifferent(sourceFile, targetFile))
                        CopyFile(_syncFrom, targetPath);
                    else
                        syncLog.Add("File not copied because source not changed");
                }
                catch
                {
                    CopyFile(_syncFrom, targetPath);
                }
            }
        }

        private void CopyFile(string source, string dest)
        {
            if (_itemMetaInfo.Count == 0)
            {
                string fileUrl = "http:" + source.Replace("\\", "/");
                SPFile file = GetFileByUrl(fileUrl);
                // if the directory contains _file usually because it is used with mht then Item will be null so skip it
                if (file.Item != null)
                {
                    LoopFieldCols(file.Item, _sourceFieldCols);
                }
            }

            // nroot - 4.20.2011 - added this catch to UrlEncode to deal with special chars
            dest = dest.Replace("+", "%252b"); // double URL encode "+" character to get it through the RPC call without being turned into a space

            try
            {
                WebClient webClient = new WebClient();
                //

                UploadDocumentResponse uploadDocumentResponse = webClient.UploadDocument(dest, source, _itemMetaInfo, PutOptionEnum.Overwrite, true);
                if (_useSyncListMetaData == false)
                    _itemMetaInfo = new MetaInfoCollection();
                if (uploadDocumentResponse.HasError)
                {
                    if (_isfile)
                        _success = false;
                    syncLog.Add("Unable to create file (error): " + uploadDocumentResponse.ErrorMessage);
                }
                else
                {
                    syncLog.Add(dest);
                }
            }
            catch (Exception ex)
            {
                if (_isfile)
                    _success = false;
                syncLog.Add("Unable to create file (exception): " + ex.Message);
            }
        }

        private void DeleteItemsNotInSource()
        {
            _existingTargetFiles.Sort(FileCompare);

            foreach (FileInfo existingFile in _existingTargetFiles)
            {
                try
                {
                    existingFile.Delete();
                    syncLog.Add("Deleted - not found in source: " + existingFile.FullName);
                }
                catch (IOException ex)
                {
                    syncLog.Add("Error deleting: " + ex.Message);
                }
            }

            _existingTargetDirectories.Sort(DirCompare);

            foreach (DirectoryInfo existingDirectory in _existingTargetDirectories)
            {
                try
                {
                    FileSystemInfo[] dirContents = existingDirectory.GetFileSystemInfos();
                    if (dirContents.Length == 0)
                    {
                        existingDirectory.Delete();
                        syncLog.Add("Deleted - directory empty: " + existingDirectory.FullName);
                    }
                }
                catch (IOException ex)
                {
                    syncLog.Add("Error deleting: " + existingDirectory.FullName + ex.Message);
                }
            }
        }

        private bool ShouldCopyFile(FileInfo sourceFile)
        {
            //check against all existing files
            foreach (FileInfo targetFile in _existingTargetFiles)
            {
                //skip hidden files
                if ((targetFile.Attributes & FileAttributes.Hidden) != FileAttributes.Hidden)
                {
                    //check to see if its the same filename
                    if (SameFile(sourceFile, targetFile))
                    {
                        _existingTargetFiles.Remove(targetFile);
                        if (_forceOverwrite)
                        {
                            return true;
                        }
                        return FilesDifferent(sourceFile, targetFile);
                    }
                }
                else
                {
                    return false;
                }
            }
            //doesn't exist in target so copy
            return true;
        }

        private bool FilesDifferent(FileInfo source, FileInfo target)
        {
            if (source.FullName.StartsWith("\\\\sharepoint\\") || source.FullName.StartsWith("\\\\jk-sharepoint"))
            {
                return SPFilesDifferent("http:" + source.FullName.Replace("\\", "/"), target);
            }
            else
            {
                // if one is null, they are not equal
                if (source == null || target == null)
                {
                    return true;
                }
                // compare date modified
                if (source.LastWriteTime >= target.LastWriteTime)
                {
                    return true;
                }
            }
            return false;
        }

        private bool SPFilesDifferent(string sURL, FileInfo target)
        {
            SPFile source = GetFileByUrl(sURL);

            // if one is null, they are not equal
            if (source == null || target == null)
            {
                return true;
            }
            // compare date modified
            if (source.TimeLastModified >= target.LastWriteTime)
            {
                return true;
            }
            return false;
        }

        private void SetItemSyncPaths(SPListItem item)
        {
            try
            {
                _syncFrom = new SPFieldUrlValue(item["SyncFrom"].ToString()).Url.Replace("%20", " ").Remove(0, 5).Replace("/", "\\");
                _syncTo = new SPFieldUrlValue(item["SyncTo"].ToString()).Url.Replace("%20", " ");
            }
            catch (Exception ex)
            {
                throw new Exception("'Sync From' or 'Sync To' path is invalid");
            }

            if (Path.GetFileName(_syncFrom) == string.Empty || Path.GetFileName(_syncFrom).IndexOf('.') == -1)
            {
                _isfile = false;
                if (_syncFrom.EndsWith("\\") == false)
                    _syncFrom = _syncFrom + "\\";
            }

            if (_syncTo.EndsWith("/") == false)
                _syncTo = _syncTo + "/";
        }

        private void SyncComplete(SPListItem item)
        {
            CleanUpZipFile(item);
            string status = "Success";
            if (_success == false)
                status = "Failed";

            if (_isfile == false && _success == true)
                DeleteItemsNotInSource();

            if (status != "Failed")
            {
                foreach (string logLine in syncLog)
                {
                    if (logLine.Contains("Unable to "))
                    {
                        status = "Incomplete";
                        break;
                    }
                }
            }
            //only set this when sync is successfull
            if (status != "Failed")
            {
                item["LastSync"] = DateTime.Now;
            }

            item["SyncStatus"] = status;
            TimeSpan runTime = (DateTime.Now).Subtract(_timeToRun);
            item["SyncLog"] = string.Join("\n", syncLog.ToArray() as string[]) + "\nRun time: " + runTime.TotalMinutes.ToString() + " minutes";
            item.SystemUpdate();
        }

        private void SyncError(SPListItem item, string logEntry)
        {
            syncLog.Add(logEntry);
            _success = false;
            SyncComplete(item);
        }

        //checks to make sure we are comparing the same filename and path
        private bool SameFile(FileInfo sourceFile, FileInfo targetFile)
        {
            bool sameName = targetFile.Name == sourceFile.Name;
            if (_isfile == false)
            {
                string syncToUnc = _syncTo.Remove(0, 5).Replace("/", "\\");
                string targetDirName = targetFile.DirectoryName.Replace(syncToUnc.TrimEnd('\\'), string.Empty);
                string sourceDirName = sourceFile.DirectoryName.Replace(_syncFrom.TrimEnd('\\'), string.Empty);
                bool sameDir = targetDirName == sourceDirName;

                return sameName && sameDir;
            }
            else
            {
                if (sameName == true)
                {
                    if (targetFile.LastWriteTime >= sourceFile.LastWriteTime)
                        return true;
                }
                return false;
            }
        }

        private bool UserCreatedField(SPField field)
        {
            if (field.Hidden == true)
                return false;
            if (field.ReadOnlyField == true)
                return false;
            if (field.InternalName.StartsWith("_") == true)
                return false;
            if (_noreadCol.Contains(field.InternalName) == true)
                return false;

            return true;
        }

        private void CreateFieldBlacklist()
        {
            //our cols
            _noreadCol.Add("SyncTo");
            _noreadCol.Add("SyncFrom");
            _noreadCol.Add("SyncLog");
            _noreadCol.Add("SyncStatus");
            _noreadCol.Add("LastSync");
            _noreadCol.Add("CreateZip");
            //cols that come with sp
            _noreadCol.Add("Attachments");
            _noreadCol.Add("ContentTypeId");
            _noreadCol.Add("ContentType");
            _noreadCol.Add("Created");
            _noreadCol.Add("Author");
            _noreadCol.Add("Modified");
            _noreadCol.Add("Editor");
            _noreadCol.Add("FileRef");
            _noreadCol.Add("FileDirRef");
            _noreadCol.Add("Last_x0020_Modified");
            _noreadCol.Add("Created_x0020_Date");
            _noreadCol.Add("File_x0020_Size");
            _noreadCol.Add("FSObjType");
            _noreadCol.Add("PermMask");
            _noreadCol.Add("CheckedOutUserId");
            _noreadCol.Add("IsCheckedoutToLocal");
            _noreadCol.Add("CheckoutUser");
            _noreadCol.Add("FileLeafRef");
            _noreadCol.Add("UniqueId");
            _noreadCol.Add("ProgId");
            _noreadCol.Add("ScopeId");
            _noreadCol.Add("VirusStatus");
            _noreadCol.Add("CheckedOutTitle");
            _noreadCol.Add("LinkCheckedOutTitle");
            _noreadCol.Add("Modified_x0020_By");
            _noreadCol.Add("Created_x0020_By");
            _noreadCol.Add("File_x0020_Type");
            _noreadCol.Add("HTML_x0020_File_x0020_Type");
            _noreadCol.Add("LinkFilenameNoMenu");
            _noreadCol.Add("LinkFilename");
            _noreadCol.Add("DocIcon");
            _noreadCol.Add("ServerUrl");
            _noreadCol.Add("EncodedAbsUrl");
            _noreadCol.Add("BaseName");
            _noreadCol.Add("FileSizeDisplay");
            _noreadCol.Add("MetaInfo");
            _noreadCol.Add("SelectTitle");
            _noreadCol.Add("SelectFilename");
            _noreadCol.Add("Edit");
            _noreadCol.Add("owshiddenversion");
            _noreadCol.Add("InstanceID");
            _noreadCol.Add("Order");
            _noreadCol.Add("GUID");
            _noreadCol.Add("Title");
            _noreadCol.Add("WorkflowVersion");
            _noreadCol.Add("WorkflowInstanceID");
            _noreadCol.Add("ParentVersionString");
            _noreadCol.Add("ParentLeafName");
            _noreadCol.Add("TemplateUrl");
            _noreadCol.Add("xd_ProgID");
            _noreadCol.Add("xd_Signature");
            _noreadCol.Add("Combine");
            _noreadCol.Add("RepairDocument");
        }

        //helper for sorting directory names
        private int DirCompare(object x, object y)
        {
            DirectoryInfo d1 = x as DirectoryInfo;
            DirectoryInfo d2 = y as DirectoryInfo;
            return d2.FullName.CompareTo(d1.FullName);
        }

        //helper for sorting filenames
        private int FileCompare(object x, object y)
        {
            FileInfo f1 = x as FileInfo;
            FileInfo f2 = y as FileInfo;
            return f2.FullName.CompareTo(f1.FullName);
        }

        private object GetFormattedMetaValue(SPListItem item, SPField field)
        {
            switch (field.Type.ToString())
            {
                case "Boolean":
                    return bool.Parse(item[field.InternalName].ToString());
                case "URL":
                    return item[field.InternalName].ToString();
                case "DateTime":
                    return ((DateTime)item[field.InternalName]).ToString("s") + "Z";
                case "User":
                    try
                    {
                        SPFieldUserValue userValue = new SPFieldUserValue(item.Web, item[field.InternalName].ToString());
                        return userValue.User.LoginName;
                    }
                    catch
                    {
                        return string.Empty;
                    }
                case "MultiChoice":
                    return item[field.InternalName].ToString();
                case "Choice":
                    return item[field.InternalName].ToString();
                default:
                    return item.GetFormattedValue(field.InternalName);
            }
        }

        private MetaTypeEnum SetMetaEnum(string type)
        {
            switch (type)
            {
                case "Boolean":
                    return MetaTypeEnum.Boolean;
                case "Integer":
                    return MetaTypeEnum.Integer;
                case "Note":
                    return MetaTypeEnum.LongText;
                case "Number":
                    return MetaTypeEnum.Double;
                default:
                    return MetaTypeEnum.String;
            }
        }

        private SPFile GetFileByUrl(string fileUrl)
        {
            try
            {
                using (SPSite fileSite = new SPSite(fileUrl))
                {
                    // Check for null
                    if (fileSite == null)
                    {
                        throw new SPException("Unable to locate site.");
                    }

                    // Get the List Web
                    using (SPWeb fileWeb = fileSite.OpenWeb())
                    {
                        // Check for null
                        if (fileWeb == null)
                        {
                            throw new SPException("Unable to open web.");
                        }

                        //string url = itemUrl.Replace("http://" + itemWeb.Site.HostName, "");
                        // Get the file
                        SPFile theFile = fileWeb.GetFile(fileUrl);

                        // Check for null
                        if (theFile == null)
                        {
                            throw new SPException("Unable to find the file.");
                        }
                        return theFile;
                    }
                }
            }
            catch (Exception ex)
            {
                throw new Exception("Unable to get or find file from " + fileUrl);
            }
        }
    }
}